/*-
 * TPDU.java
 *
 * $LastChangedBy: stein $
 * $LastChangedRevision: 96 $
 * $LastChangedDate: 2007-02-15 19:53:47 +0000 (Thu, 15 Feb 2007) $
 * $HeadURL: svn://localhost/SMS/src/com/googlecode/attention/sms/pdu/TPDU.java $
 *
 * Copyright (c) 2007, Stein Gunnar Bakkeby
 * All rights reserved.
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of Stein Gunnar Bakkeby nor the names of its
 *       contributors may be used to endorse or promote products derived
 *       from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.googlecode.attention.sms.pdu;

import static com.googlecode.attention.sms.SMSLogStatements.*;

import com.googlecode.attention.util.Convert;
import com.googlecode.attention.util.IntArrayBuilder;
import com.googlecode.attention.util.KeepingReference;


/** * PDU Type repertoire at SM-TL. * <BR> * <BR>The SM-TL comprises the following six PDUs: * <BR> * <BR>SMS-DELIVER, conveying a short message from the SC to the MS; * <BR>SMS-DELIVER-REPORT, conveying; * <BR> a) a failure cause (if necessary); * <BR> b) information as part of a positive or negative acknowledgement to an *         SMS-DELIVER or SMS-STATUSREPORT; * <BR>SMS-SUBMIT, conveying a short message from the MS to the SC; * <BR>SMS-SUBMIT-REPORT, conveying; * <BR> a) a failure cause (if necessary); * <BR> b) information as part of a positive or negative acknowledgement to *         an SMS-SUBMIT or SMSCOMMAND; * <BR>SMS-STATUS-REPORT, conveying a status report from the SC to the MS; * <BR>SMS-COMMAND, conveying a command from the MS to the SC. *  * @author Stein Gunnar Bakkeby */
public abstract class TPDU implements Comparable<TPDU> {
  
  /* Static field values */
  /**
   * Default init size for new arrays of TPDUs,
   * could be used for performance tweaking.
   */
  protected static final int INIT_SIZE = 256;
  
  public static final int SMS_DELIVER = 1;
  public static final int SMS_SUBMIT  = 2;
  public static final int SMS_DELIVER_REPORT = 3;
  public static final int SMS_SUBMIT_REPORT = 4;
  public static final int SMS_STATUS_REPORT = 5;
  public static final int SMS_COMMAND = 6;
  /* End static field values */

  /* Field values */
  /**
   * The raw data of the PDU.
   */
  protected IntArrayBuilder pdu = new IntArrayBuilder(INIT_SIZE);
  protected UD ud = null;

  protected int pduPointer  = 0;
  private final int type;
  private Address smscInfo = null;
  protected int[] rest = null;
  
  
  /**
   * Default constructor, used by extending classes.
   * 
   * @param pdu The raw data
   */
  protected TPDU(final String pdu, final int type) {
    setPDU(pdu);
    this.type = type;
  }
  
  /**
   * Constructor, creates the TPDU.
   * 
   * @param pdu The raw data in int[] format
   */
  protected TPDU(final int[] pdu, final int type) {
    this.pdu = new IntArrayBuilder(pdu);
    this.pduPointer = 0;
    this.type = type;
  }
  
  /**
   * This should return the relevant number for this message. E.g. if this
   * is an SMS delivery message then this method should return the originators
   * address, while if it is an SMS submit then the destination address should
   * be returned. If the SMS is an SMS report then a null value will be
   * returned.
   * 
   * @return Relevant phone number / address or null if SMS report
   */
  public abstract Address getAddress();
  
  /**
   * This should set the relevant number for this message. E.g. if this
   * is an SMS delivery message then this method should set the originator
   * address, while if it is an SMS submit then the destination address should
   * be set. If the SMS is an SMS report then argument will be ignored.
   * 
   * @param address Relevant phone number / address
   */
  public abstract void setAddress(final Address address);
  
  /**
   * This will return the SMS type, see static field values.
   * 
   * @return The SMS type
   */
  public int getType() {
    return type;
  }
  
  /**
   * This will create the PDU from the raw data.
   * <BR>
   * <BR>This applies for:
   * <BR>
   * <BR><B>In direction from SC to MS:</B>
   * <BR> - SMS-DELIVER
   * <BR> - SMS-SUBMIT-REPORT
   * <BR> - SMS-STATUS-REPORT
   * <BR> - RESERVED
   * <BR>
   * <BR><B>In direction from MS to SC:</B>
   * <BR> - SMS-DELIVER-REPORT
   * <BR> - SMS-SUBMIT
   * <BR> - SMS-COMMAND
   * <BR> - RESERVED
   * 
   * @param pdu The raw data (expected hex where 2 chars = 1 byte)
   * @param direction See static field values 
   * @param ack Used in SMS-DELIVER-REPORT and SMS-SUBMIT-REPORT, specifies
   *            if this PDU is a RP-ACK (true) or a RP-ERROR (false). For all
   *            other PDU's, set this to true.
   * @param hasSMSCInfo Indicates whether the received PDU includes SMSC info
   * @see #getPDU(int[], MTI.DIRECTION, boolean, Address)
   * @return The TPDU object
   * @throws IllegalArgumentException If PDU has errors or MTI group reserved
   */
  public static TPDU getPDU(final String pdu,
                            final MTI.DIRECTION direction,
                            final boolean ack,
                            final boolean hasSMSCInfo)
                            throws IllegalArgumentException {
    if (hasSMSCInfo) {
      final int smscInfoLength = Convert.hexToInt(pdu.substring(0, 2));
      final int index = smscInfoLength * 2 + 2;
      final Address smscInfo = new Address(pdu.substring(0, index));
      return getPDU(pdu.substring(index), direction, ack, smscInfo);
    }
    return getPDU(pdu, direction, ack, null);
  }
                            
  
  /**
   * This will create the PDU from the raw data.
   * <BR>
   * <BR>This applies for:
   * <BR>
   * <BR><B>In direction from SC to MS:</B>
   * <BR> - SMS-DELIVER
   * <BR> - SMS-SUBMIT-REPORT
   * <BR> - SMS-STATUS-REPORT
   * <BR> - RESERVED
   * <BR>
   * <BR><B>In direction from MS to SC:</B>
   * <BR> - SMS-DELIVER-REPORT
   * <BR> - SMS-SUBMIT
   * <BR> - SMS-COMMAND
   * <BR> - RESERVED
   * 
   * @param pdu The raw data (expected hex where 2 chars = 1 byte)
   * @param direction See static field values 
   * @param ack Used in SMS-DELIVER-REPORT and SMS-SUBMIT-REPORT, specifies
   *            if this PDU is a RP-ACK (true) or a RP-ERROR (false). For all
   *            other PDU's, set this to true. 
   * @param smscInfo The SMSC info, set to null if not present
   * @see #getPDU(int[], MTI.DIRECTION, boolean, Address)
   * @return The TPDU object
   * @throws IllegalArgumentException If PDU has errors or MTI group reserved
   */
  public static TPDU getPDU(final String pdu,
                            final MTI.DIRECTION direction,
                            final boolean ack,
                            final Address smscInfo)
                            throws IllegalArgumentException {
    final int length = pdu.length();
    final int tpdu[] = new int[length / 2];
    int index = 0;
    for (int i = 0; i <= length - 2; i += 2) {
      tpdu[index++] = Integer.parseInt(pdu.substring(i, i + 2), 16);
    }
    return getPDU(tpdu, direction, ack, smscInfo);
  }
  
  
  /**
   * This will create the PDU from the raw data.
   * <BR>
   * <BR>This applies for:
   * <BR>
   * <BR><B>In direction from SC to MS:</B>
   * <BR> - SMS-DELIVER
   * <BR> - SMS-SUBMIT-REPORT
   * <BR> - SMS-STATUS-REPORT
   * <BR> - RESERVED
   * <BR>
   * <BR><B>In direction from MS to SC:</B>
   * <BR> - SMS-DELIVER-REPORT
   * <BR> - SMS-SUBMIT
   * <BR> - SMS-COMMAND
   * <BR> - RESERVED
   * 
   * @param pdu The raw data
   * @param direction See static field values 
   * @param ack Used in SMS-DELIVER-REPORT and SMS-SUBMIT-REPORT, specifies
   *            if this PDU is a RP-ACK (true) or a RP-ERROR (false). For all
   *            other PDU's, set this to true. 
   * @return The TPDU object
   * @throws IllegalArgumentException If PDU has errors or MTI group reserved
   */
  public static TPDU getPDU(int[] pdu,
                            final MTI.DIRECTION direction,
                            final boolean ack,
                            final Address smscInfo)
                            throws IllegalArgumentException {
    if (pdu.length == 0) throw new IllegalArgumentException("No PDU");
    
    final MTI mti = MTI.parse(pdu[0] & 0x3, direction); // (B1|B0)
    final TPDU output;
     
    switch (mti) {
    case SMS_DELIVER_REPORT:
      output = new SMSDeliverReport(pdu, ack);
      break;
      
    case SMS_SUBMIT:
      output = new SMSSubmit(pdu);
      break;
      
    case SMS_COMMAND:
      output = new SMSCommand(pdu);
      break;
      
    case RESERVED:
      throw new IllegalArgumentException("MTI group reserved");
      
    case SMS_DELIVER:
      output = new SMSDeliver(pdu);
      break;

    case SMS_SUBMIT_REPORT:
      output = new SMSSubmitReport(pdu, ack);
      break;
      
    case SMS_STATUS_REPORT:
      output = new SMSStatusReport(pdu);
      break;
      
    default: // should never happen
      throw new IllegalArgumentException("PDU has errors: MTI group");
    }
    
    if (smscInfo != null) output.setSMSCInfo(smscInfo);
    return output;
  }
  
  /**
   * This will set the SMSC info.
   * 
   * @param smscInfo SMSC address
   */
  public void setSMSCInfo(Address smscInfo) {
    this.smscInfo = smscInfo;
  }
  
  /**
   * This indicates whether the SMS message includes SMSC info.
   * 
   * @return True if SMSC info is present, false otherwise
   */
  public boolean hasSMSCInfo() {
    return smscInfo != null;
  }
  
  /**
   * This will return the SMSC info, or null if not present.
   * 
   * @return The SMSC info, or null if not present
   */
  public Address getSMSCInfo() {
    return smscInfo;
  }

  /**
   * This will encode the PDU using the current settings.
   */
  public abstract void encode();
  
  /**
   * This will decode the PDU.
   */
  public abstract void decode();
  
  /**
   * This will return the SMS type, see MTI static field values.
   * 
   * @return Integer representing the SMS type, defined by the MTI
   */
  public int getSMSType() {
    return pdu.intAt(0) & 0x3; // (B1|B0)
  }
  
  /**
   * Extracts an octet from the PDU.
   * <BR>
   * <BR>The method uses a pointer which is updated for each time
   * the extractOctet() method is invoked. The method returns a
   * substring of the PDU raw data, if there are any more data to 
   * retrieve. In case there are not anymore data to retrieve yet
   * it should be by the PDU format, then an empty octet will be
   * returned ("00").
   *
   * @return The extracted octet from the PDU raw data
   */
  protected int extractOctet() {
    /* Ensures that there is enough data to retrieve */
    if (pduPointer < pdu.length()) {
      return pdu.intAt(pduPointer++);
    } // else data deficiency
    SMS_001.log(pdu);
    return 0;
  }
  
  /**
   * This will return true if there are more octets to extract.
   * 
   * @return True if there are more octets to extract, false otherwise
   */
  protected boolean hasMoreOctets() {
    return (pduPointer < pdu.length());
  }

  /**
   * Extracts a number of octets from the PDU raw data.
   * <BR>
   * <BR>The method uses a pointer which is updated for each time
   * the extractOctet() method is invoked. The method returns a
   * substring of the PDU raw data, if there are any more data to 
   * retrieve. In case there are not anymore data to retrieve yet
   * it should be by the PDU format, then an empty octets will be
   * returned ("00").
   *
   * @param numberOfOctets The number of octets to extract
   * @return The octets extracted from the PDU raw data
   */
  protected int[] extractOctets(final int numberOfOctets) {
    final int[] ret = new int[numberOfOctets];
    /* to prevent index out of bounds in case of error */
    for (int i = 0; i < numberOfOctets; i++) {
      ret[i] = extractOctet();
    }
    return ret;
  }
  
  protected int[] getRest() {
    return (extractOctets(pdu.length() - pduPointer));
  }
  
  /**
   * This will reset the PDU pointer to 0.
   */
  protected void resetPDUPointer() {
    pduPointer = 0;
  }
  
  /**
   * This will return the raw PDU, if a new PDU is being created, make sure
   * to encode the PDU first.
   * 
   * @return This will return the PDU string, the PDU must be encoded first
   * @see #encode()
   */
  public int[] getPDU() {
    return pdu.toArray();
  }
  
  /**
   * This will return the raw PDU as a String, if a new PDU is being created,
   * make sure to encode the PDU first.
   * 
   * @return This will return the PDU string, the PDU must be encoded first
   * @see #encode()
   */
  public String getPDUAsString() {
    return Convert.intToHex(pdu.toArray());
  }
  
  /**
   * Returns a String representation of the PDU.
   * 
   * @return The PDU in String/hex representation ( 2 char = 1 byte )
   */
  public String toString() {
    return Convert.intToHex(pdu);
  }
  
  /**
   * This will set the PDU for this object.
   * 
   * @param pdu The pdu to set
   */
  public void setPDU(final String pdu) {
    final int length = pdu.length();
    final int pduLength  = length / 2;
    this.pdu = new IntArrayBuilder(pduLength);
    for (int i = 0; i < length; i += 2) {
      this.pdu.append(Integer.parseInt(pdu.substring(i, i + 2), 16));
    }
    this.pduPointer = 0;
  }
  
  /**
   * This will return an element of a given type.
   * 
   * @param type See static field values
   * @return The element in this TPDU
   */
  public abstract int get(final PDUElement type);
  
  /**
   * This will return the SMS user-data. Note that this is an integer encoded
   * array.
   * 
   * @return The TP-User-Data
   * @see #getMessage()
   */
  public UD getUserData() {
    return (ud == null ? new UD() : ud);
  }
  
  /**
   * This will return the user data header.
   * 
   * @return The TP-UDH
   */
  public UDH getUserDataHeader() {
    return getUserData().getUDH();
  }
  
  /**
   * This will set the user data.
   * 
   * @param userData The UD of this status report.
   */
  @KeepingReference
  public void setUserData(final UD userData) {
    this.ud = userData;
  }
  
  /**
   * This will set the user data header, note that this method will throw
   * an IllegalStateException if the header is being set before the user data
   * itself is set.
   * 
   * @param userDataHeader The user data header to set.
   * @throws IllegalStateException If no user data exist
   * @see UD#setUDH(UDH)
   */
  @KeepingReference
  public void setUserDataHeader(final UDH userDataHeader) {
    if (this.ud == null) {
      throw new IllegalStateException("User Data has not been set!");
    }
    this.ud.setUDH(userDataHeader);
  }
  
  /**
   * This will convert the user data into properly readable data.
   * 
   * @return The TP-UD
   */
  public String getMessage() {
    return new String(Convert.intToChar(getUserData().toArray()));
  }
  
  /**
   * This will set an element of a given type.
   * 
   * @param type See static field values
   * @param value The value of the element to set
   * @return True if PDU contains the given type, false otherwise
   */
  public abstract boolean set(final PDUElement type, final int value);
  
  /**
   * This will return true if the PDU contains the given type.
   * 
   * @param type See TPDU static field values
   * @return True if given type is present, false otherwise
   */
  public abstract boolean contains(final PDUElement type);
  
  public void setRest(final int[] rest) {
    if (rest == null || rest != null && rest.length == 0) {
      this.rest = null;
    } else {
      this.rest = rest;
    }
  }
  
  public int compareTo(final TPDU another) {
    return getMessage().compareTo(another.getMessage());
  }
}